import functools
import logging
import re
import types
from enum import Enum

from app.common.cache import get_redis
from app.common.errors import ApiError
from app.common.errors import Error
from settings import Config

logger = logging.getLogger(__name__)


class BaseDecorator:
    def __init__(self, func):
        functools.wraps(func)(self)

    def __call__(self, *args, **kwargs):
        return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)


class PermissionRequired(BaseDecorator):
    def __init__(self, permission_enum: Enum):
        self.permission_enum = permission_enum

    def __call__(self, func):
        @functools.wraps(func)
        async def deco(*args, **kwargs):
            from app.common.auth import TokenUser
            from app.common.auth import get_token_user

            info = args[-1]
            token_user: TokenUser = get_token_user(info.context["request"])
            if not token_user:
                raise ApiError(Error.NEED_LOGIN)
            if (
                not token_user.permissions
                or self.permission_enum.value not in token_user.permissions
            ):
                raise ApiError(Error.PERMISSION_ERROR)
            return func(*args, **kwargs)

        return deco


# decorator with parameters
class GetOrSetInCache(BaseDecorator):
    """
    这是用于在多个地方管理同一个缓存数据对象的装饰器类，用于对查询数据的装饰 \n
    init parameters:
        cache_key_template: 如"user_followee_ids_{user_id}" 根据括号内的参数去装饰的func内寻找参数并渲染 \n
        force_update: 强制更新缓存 \n
        timeout:缓存timeout \n
        filter_func: 过滤当前被装饰函数的返回值并取其需要的数据存入缓存，
        缓存与函数返回数据不一致，仅用作set而不get缓存
    """

    def __init__(
        self,
        cache_key_template,
        force_update=False,
        timeout=Config.CACHE_TIMEOUT,
        filter_func=None,
    ):
        self.cache_key_template = cache_key_template
        self.force_update = force_update
        self.timeout = timeout
        self.filter_func = filter_func

    def __call__(self, func):
        @functools.wraps(func)
        async def deco(*args, **kwargs):
            cache_key_args = set(re.findall(r"{(.*)}", self.cache_key_template))
            assert cache_key_args.issubset(set(kwargs.keys())), "cache参数错误"
            cache_key = self.cache_key_template.format(
                **{cache_arg: kwargs[cache_arg] for cache_arg in cache_key_args}
            )

            default_filter_func = lambda x: x  # noqa: E731
            async with get_redis() as redis:

                if self.force_update is True or not await redis.exists(cache_key):
                    res = await func(*args, **kwargs)
                    set_cache_data = (
                        self.filter_func(res)
                        if self.filter_func
                        else default_filter_func(res)
                    )
                    redis_pip = redis.pipeline()
                    redis_pip.sadd(cache_key, *set_cache_data, "None")
                    redis_pip.expire(cache_key, self.timeout)
                    await redis_pip.execute()
                    # await redis.set(cache_key, set_cache_data, timeout=self.timeout)
                else:
                    res = set(await redis.smembers(cache_key))
            res.discard("None")
            return res

        return deco
